#!/usr/bin/python -S
"""
A test for demonstrating how obscure regex features work.
"""

import unittest

import re  # module under test


def ShowMatch(*args):
  match = re.match(*args)
  if match:
    print match.group(0)
  else:
    print 'no match'

# http://perldoc.perl.org/perlre.html#Regular-Expressions
#
# (?adlupimsx-imsx)
# (?^alupimsx)
#
# One or more embedded pattern-match modifiers, to be turned on (or turned off,
# if preceded by - ) for the remainder of the pattern or the remainder of the
# enclosing pattern group (if any).  This is particularly useful for dynamic
# patterns, such as those read in from a configuration file, taken from an
# argument, or specified in a table somewhere. Consider the case where some
# patterns want to be case-sensitive and some do not: The case-insensitive ones
# merely need to include (?i) at the front of the pattern. For example:
#
#
# (?^aluimsx:pattern)
#
# This is for clustering, not capturing; it groups subexpressions like "()", but
# doesn't make backreferences as "()" does. So
#
# SUMMARY: Perl has two modes.  (?i)abc and (?i:abc)
#
# The first sets flags in the current group.
# The second defines a group?
#
# In Python, we only have a global setting:
#
# http://docs.python.org/2/library/re.html
#
# Note that the (?x) flag changes how the expression is parsed. It should be
# used first in the expression string, or after one or more whitespace
# characters. If there are non-whitespace characters before the flag, the
# results are undefined.


class ReTest(unittest.TestCase):

  def testFlags(self):
    ShowMatch('a', 'a')
    ShowMatch('a', 'A')

    # In Python, Setting flags has to be the FIRST thing in the string.
    ShowMatch('(?i)a', 'a')
    ShowMatch('(?i)a', 'A')

    # CRE syntax:
    #
    # set ignorecase locale dotall multiline unicode
    #
    # 'verbose' is not necessary anymore, since we're using CRE.
    #
    # dotall -> anyall

  def testAssertions(self):
    ShowMatch(r'(?=\w)h', 'hello')
    ShowMatch(r'(?!\w)h', 'hello')
    ShowMatch(r'h(?<=\w)', 'hello')
    ShowMatch(r'h(?<!\w)', 'hello')

    # This is a weird assertion
    ShowMatch(r'(foo)?(?(1)foo|bar)', 'foofoo')
    ShowMatch(r'(foo)?(?(1)foo|bar)', 'foobar')
    ShowMatch(r'(foo)?(?(1)foo|bar)', 'foo')
    ShowMatch(r'(foo)?(?(1)foo|bar)', 'bar')

  def testBackref(self):
    ShowMatch(r'he(\w)\1o', 'hello')
    ShowMatch(r'he(\w)\1o', 'helfo')

    ShowMatch(r'he(?P<name>\w)(?P=name)', 'hello')
    ShowMatch(r'he(?P<name>\w)(?P=name)', 'helfo')

  def testSub(self):
    r = re.compile(r'\$(\d+)')
    # \0 is a NUL byte, not the whole match!  Inconsistent.
    print r.sub(r'#\0#', r'abc $123 def')

    print r.sub(r'#\1#', r'abc $123 def')
    print r.sub(r'#\g<1>#', r'abc $123 def')
    print r.sub(r'#\g<0>#', r'abc $123 def')

    # Test named substitution.
    r = re.compile(r'\$(?P<num>\d+)')
    print r.sub(r'#\g<0>#', r'abc $123 def')
    print r.sub(r'#\g<1>#', r'abc $123 def')
    print r.sub(r'#\g<num>#', r'abc $123 def')

    def rep(m):
      return '!!' + m.group(0) + '!!'

    print r.sub(rep, r'abc $123 def')

    # raises IndexError -- not a great API.
    #print r.sub(r'#\g<foo>#', r'abc $123 def')

  def testExceptions(self):
    try:
      re.compile('[a-')
    except Exception, e:
      print type(e), e

    # unbalanced
    try:
      re.compile('(foo')
    except Exception, e:
      print type(e), e

    # unbalanced
    try:
      re.compile('(?:foo')
    except Exception, e:
      print type(e), e

  def testFind(self):
    # There can be multiple matches in general.
    d = re.compile(r'\d+')
    print d.findall('abc 123 def 45.')

    # 1 group
    d = re.compile(r' (\d+)')
    print d.findall('abc 123 def 45.')

    # 2 groups.  We get tuples.  Is there no way to get the positions?  This is
    # a non-orthogonal API.
    d = re.compile(r'( )(\d+)')
    print d.findall('abc 123 def 45.')

  def testSplit(self):
    d = re.compile(r'\d+')
    print d.split('abc 123 def 45.')

    # Now the numbers are included
    d = re.compile(r'(\d+)')
    print d.split('abc 123 def 45.')

    # Splits into even more groups.  Interesting.
    d = re.compile(r'( )(\d+)')
    print d.split('abc 123 def 45.')

    # Omitting 1 digit. 
    d = re.compile(r'( )\d(\d+)')
    print d.split('abc 123 def 45.')

  def testMatchAll(self):
    # There can be multiple matches in general.
    greedy = re.compile('a+b+')
    non = re.compile('a+?b+?')
    non_full = re.compile('a+?b+?$')
    def Pair(s):
      print 'greedy', greedy.match(s).group(0)
      print 'non-greedy', non.match(s).group(0)
      print 'non-greedy full', non_full.match(s).group(0)
      print

    # TODO: remove .matchall(), or document that it causes another compilation
    # step?
    Pair('ab')
    Pair('abb')

  def testUnicode(self):
    # single unicode char
    ur = re.compile(u'.$')

    # single byte
    sr = re.compile('.$')

    # char range
    urange = re.compile(u'[\u03bc-\u03bd]$')  # if I add 'ur' it still works
    byterange = re.compile('[\x00-\x01]$')

    wordchar = re.compile(r'\w$')
    u_wordchar = re.compile(r'(?u)\w$')

    utf8 = re.compile('\xce\xbc$')

    mixed = re.compile(u'[\x00\u03bc]$')


    def match(r, c):
      m = r.match(c)
      if m:
        return repr(m.group(0))
      else:
        return 'None'

    fmt = '%20s %10s %10s'
    cases = [
        '\x00',
        '\x00\x01',  # 2 chars
        u'a',
        u'\u03bc',  # mu
        # utf-8 encoding of mu.  This is TWO CHARS.
        '\xce\xbc',
        ]
    for c in cases:
      row = repr(ur.pattern), repr(c), match(ur, c)
      print fmt % row

      row = repr(sr.pattern), repr(c), match(sr, c)
      print fmt % row

      row = repr(urange.pattern), repr(c), match(urange, c)
      print fmt % row

      row = repr(byterange.pattern), repr(c), match(byterange, c)
      print fmt % row

      row = repr(utf8.pattern), repr(c), match(utf8, c)
      print fmt % row

      row = repr(wordchar.pattern), repr(c), match(wordchar, c)
      print fmt % row

      row = repr(u_wordchar.pattern), repr(c), match(u_wordchar, c)
      print fmt % row

      row = repr(mixed.pattern), repr(c), match(mixed, c)
      print fmt % row

    # CONCLUSION:
    # I don't see any weird behavior.  Or encode/decode errors?  Python's
    # unicode appears to be doing the right thing, although I don't see where
    # it's really documented.
    #
    # Strategy for annex: Can you always generate unicode re strings?
    # Or maybe adding &03bc will always do the right thing?  There's no way to
    # represent that in ASCII in Python.
    #
    # What happens when you mix \0 and \u03bd?  A: That works.  Unicode strings
    # can represent bytes... since code points = bytes.
    # 
    # the re.UNICODE flag makes \w match mu.
    #
    # API: In annex, the regex can be unicode or ascii.  If unicode, then you
    # can use \u03bc escapes in addition to &03bc.  That's the only difference.

    print u'[\u03bc\n]'
    print ur'[\u03bc\n]'  # hm r prevent \u from working!  Weird.

    # all these are the same in Python.
    print repr(u'\0')
    print repr(u'\x00')
    print repr(u'\u0000')


if __name__ == '__main__':
  unittest.main()
